// this module inspired by C#.LINQ.XDocument

package by.ad.xml;

import by.ad.Utils;
import com.google.gdata.util.common.base.StringUtil;
import java.io.InputStream;
import java.util.Date;
import java.util.Scanner;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
import static by.ad.xml.XmlUtils.ISO8601UTCFormat;
import static com.google.common.base.Charsets.UTF_8;

public class XDocument
{
  //
  // public declarations
  //
  
  public final static XElement XmlProlog = new XProcessingInstruction("xml",
    new XAttribute("version", "1.0"),
    new XAttribute("encoding", UTF_8.name())
  );
  
  public final XElement Document;
  
  //
  // Helper classes
  // 
  
  // XNode +-> XText -> XAttribute
  //       |-> XElement +-> XComment -> XIEComment
  //                    |-> XProcessingInstruction
  
  // the root class. Name can be null.
  public abstract static class XNode
  {
    public final String name;
    
    public XNode(final String name) {
      this.name = name;
    }
    
    public String toString() {
      return name != null ? name : "";
    }
  }
  
  // just a text
  public static class XText extends XNode
  {
    public final String value;
    
    public XText(final String value) {
      this(null, value);
    }
    
    protected XText(final String name, final String value) {
      super(name);
      this.value = value;
    }
    
    public String toString() {
      return value != null ? StringUtil.xmlEscape(value) : "";
    }
  }
  
  // an attribute. name="value"
  public static class XAttribute extends XText
  {
    public XAttribute(final String name, final String value) {
      super(name, value);
    }
    
    public XAttribute(final String name, final Integer value) {
      super(name, value.toString());
    }
      
    public XAttribute(final String name, final Boolean value) {
      super(name, value ? "true" : "false");
    }
    
    public String toString() {
      return " " 
        + (name != null ? name : "")
        + ((name != null && value != null) ? "=" : "")
        + (value != null ? ("\"" + StringUtil.xmlEscape(value) + "\"") : "");
    }
  }

  // an element. It can contains other nodes.
  public static class XElement extends XNode 
  {
    public final XNode[] nodes;

    public XElement(final String name, final XNode... nodes) {
      super(name);
      this.nodes = nodes;
    }
    
    public XElement(final String name, final String value) {
      this(name, new XText(value));
    }
    
    public XElement(final String name, final Date value) {
      this(name, new XText(
        new ISO8601UTCFormat().format(value)
      ));
    }
    
    public XElement add(final XNode... nodes) {
      return new XElement(this.name, Utils.concat(this.nodes, nodes));
    }
    
    public String toString() {
      final StringBuilder children = new StringBuilder();
      for (final XNode node : nodes)
        if (node != null && !(node instanceof XAttribute))
          children.append(node.toString());
          
      final StringBuilder self = new StringBuilder();
      self.append('<');
      self.append(name);
      for (final XNode node : nodes)
        if (node instanceof XAttribute)
          self.append(node.toString());
      if (children.length() > 0) {
        self.append('>');
        self.append(children);
        self.append("</" + name + ">");
      } else
        self.append("/>");
      
      return self.toString();
    }
  }

  // a commentary. It can contains other nodes too.
  public static class XComment extends XElement 
  {
    public XComment(final XNode... nodes) {
      super(null, nodes);
    }
    
    public String toString() {
      final StringBuilder self = new StringBuilder();
      self.append("<!--");
      for (final XNode node : nodes)
        if (node != null)
          self.append(node.toString());
      self.append("-->");
      
      return self.toString();
    }
  }

  // an IE conditional commentary.
  public static class XIEComment extends XComment 
  {
    public final String condition;
    
    public XIEComment(final XNode... nodes) {
      this("IE", nodes);
    }
    
    public XIEComment(final String condition, final XNode... nodes) {
      super(nodes);
      this.condition = condition;
    }
    
    public String toString() {
      final StringBuilder self = new StringBuilder();
      self.append("<!--[if " + condition + "]>");
      for (final XNode node : nodes)
        if (node != null)
          self.append(node.toString());
      self.append("<![endif]-->");
      
      return self.toString();
    }
  }

  // an instruction. example - <?xml version="1.0" encoding="UTF-8"?>
  public static class XProcessingInstruction extends XElement
  {
    public XProcessingInstruction(final String name, final XAttribute... attributes) {
      super(name, attributes);
    }
    
    public String toString() {
      final String str = super.toString(); 
      return "<?" + str.substring(1, str.length() - 2) + "?>";
    }
  }
  
  //
  // XDocument
  //

  public XDocument(final XElement... elements) {
    this.Document = new XElement(null, elements);
  }
  
  public String toString() {
    final StringBuilder self = new StringBuilder();
    for (final XNode node : Document.nodes)
      if (node != null)
        self.append(node.toString());
    return self.toString();
  }

  public XDocument buildFromCSV(final InputStream inputStream) 
  {
    // http://code.google.com/intl/ru/apis/fusiontables/docs/samples/java.html
    final Pattern CSV_VALUE_PATTERN =
      Pattern.compile("([^,\\r\\n\"]*|\"(([^\"]*\"\")*[^\"]*)\")(,|\\r?\\n)");
    final Scanner scanner = new Scanner(inputStream, UTF_8.name());
    while (scanner.hasNextLine()) {
      scanner.findWithinHorizon(CSV_VALUE_PATTERN, 0);
      final MatchResult match = scanner.match();
      final String quotedString = match.group(2);
      final String decoded = quotedString == null ? match.group(1)
          : quotedString.replaceAll("\"\"", "\"");
      
      System.out.print("|" + decoded);
      if (!match.group(4).equals(",")) {
        System.out.println('|');
      }
    }
    
    return null;
  }
  
}
